/**
 * 
 */
package edu.ou.cs.youming.services;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;

import edu.ou.cs.youming.agents.Agent;
import edu.ou.cs.youming.jaxb.types.Dictionary;
import edu.ou.cs.youming.jaxb.types.Example;
import edu.ou.cs.youming.jaxb.types.Feature;
import edu.ou.cs.youming.jaxb.types.Label;
import edu.ou.cs.youming.jaxb.types.Results;

/**
 * @author Youming Lin
 * 
 */
public final class Services {
	/**
	 * Gets the frequency of each label in the {@link Map} of {@link Feature}s.
	 * 
	 * @param features
	 * @return
	 */
	public static final Map<Label, Integer> countLabels(final Map<Feature, Label> features) {
		if (features == null) {
			throw new NullPointerException("examples");
		}

		final HashMap<Label, Integer> labelCount = new HashMap<Label, Integer>();

		for (final Entry<Feature, Label> e : features.entrySet()) {
			Integer count = labelCount.get(e.getValue());

			if (count == null) {
				count = 1;
			}
			else {
				++count;
			}

			labelCount.put(e.getValue(), count);
		}

		return labelCount;
	}

	/**
	 * Process tweets into a string of space-separated tokens for use with dictionary and feature vector generation.
	 * 
	 * @param examples
	 * @return
	 */
	public static final Iterable<Example> processTweets(final Iterable<Example> examples) {
		for (final Example e : examples) {
			e.tweet.text = e.tweet.text.toLowerCase().replaceAll("\\p{Punct}", " ");
		}

		return examples;
	}

	/**
	 * Creates a dictionary from a set of examples. Does not remove duplicate words in a tweet before adding it to the
	 * dictionary.
	 * 
	 * @param examples
	 * @return
	 */
	public static final Dictionary buildDictionary2(final Iterable<Example> examples) {
		if (examples == null) {
			throw new NullPointerException("examples");
		}

		final Dictionary dictionary = new Dictionary();

		for (final Example e : examples) {
			for (final String token : e.tweet.text.split(" ")) {
				if (token.length() > 2) {
					dictionary.addInstance(token, e.polarity);
				}
			}
		}

		return dictionary;
	}

	/**
	 * Creates a dictionary from a set of examples. Removes duplicate words in a tweet before adding to dictionary.
	 * 
	 * @param examples
	 * @return
	 */
	public static final Dictionary buildDictionary(final Iterable<Example> examples) {
		if (examples == null) {
			throw new NullPointerException("examples");
		}

		final Dictionary dictionary = new Dictionary();

		for (final Example e : examples) {
			for (final String token : new HashSet<String>(Arrays.asList(e.tweet.text
					.split(" ")))) {
				if (token.length() > 2) {
					dictionary.addInstance(token, e.polarity);
				}
			}
		}

		return dictionary;
	}

	/**
	 * Converts examples into features for use in training and validation.
	 * 
	 * @param examples
	 * @return
	 */
	public static final Map<Feature, Label> generateFeatures(
			final Iterable<Example> examples) {
		if (examples == null) {
			throw new NullPointerException("examples");
		}

		final HashMap<Feature, Label> features = new HashMap<Feature, Label>();

		for (final Example e : examples) {
			final HashMap<String, Integer> frequencies = new HashMap<String, Integer>();

			for (final String token : e.tweet.text.split(" ")) {
				Integer count = frequencies.get(token);

				if (count == null) {
					count = 0;
				}

				++count;
				frequencies.put(token, count);
			}

			features.put(new Feature(e.tweet.id, e.tweet.user.location, frequencies),
					e.polarity);
		}

		return features;
	}

	/**
	 * Generates k-fold validation results for a set of agents.
	 * 
	 * @param examples
	 * @param agents
	 * @param threshold
	 * @param k
	 * @return
	 * @throws Exception
	 */
	public static final Map<Integer, ? extends Iterable<Results>> crossValidate(
			final List<Example> examples, final Iterable<Agent> agents,
			final double threshold, final int k) throws Exception {
		if (examples == null) {
			throw new NullPointerException("examples");
		}

		if (examples.isEmpty()) {
			throw new IllegalArgumentException("no examples");
		}

		if (agents == null) {
			throw new NullPointerException("agents");
		}

		if (!agents.iterator().hasNext()) {
			throw new IllegalArgumentException("no agents");
		}

		if (threshold < 0) {
			throw new IllegalArgumentException("threshold less than 0");
		}

		if (k < 1) {
			throw new IllegalArgumentException("k less than 1");
		}

		final ArrayList<Example> remainingExamples = new ArrayList<Example>(examples);

		// separate examples into k sets
		final Random random = new Random(System.nanoTime());
		final ArrayList<ArrayList<Example>> folds = new ArrayList<ArrayList<Example>>(k);

		for (int i = 0; i < k; ++i) {
			final ArrayList<Example> fold = new ArrayList<Example>();

			while ((fold.size() < remainingExamples.size() / k)
					&& !remainingExamples.isEmpty()) {
				final int index = random.nextInt(remainingExamples.size());
				fold.add(remainingExamples.get(index));
				remainingExamples.remove(index);
			}

			folds.add(fold);
		}

		final HashMap<Integer, ArrayList<Results>> results = new HashMap<Integer, ArrayList<Results>>();

		// ////////////////////////////////////////////////////////////////////////////////
		// run k-fold cross validation
		for (int i = 0; i < k; ++i) {
			final ArrayList<Example> validationSet = folds.get(i);
			final ArrayList<Example> trainingSet = new ArrayList<Example>();

			for (int j = 0; j < folds.size(); ++j) {
				if (j != i) {
					trainingSet.addAll(folds.get(j));
				}
			}

			// step 1:
			// generate dictionary
			final Dictionary dict = buildDictionary(trainingSet);
			dict.trim(threshold);

			System.out.println(dict.sentimentValues.size());

			// step 2:
			// generate features
			final Map<Feature, Label> trainingFeatures = generateFeatures(trainingSet);
			final Map<Feature, Label> validationFeatures = generateFeatures(validationSet);

			// step 3:
			// train agents
			for (final Agent a : agents) {
				final long startTime = System.nanoTime();
				a.train(trainingFeatures, dict);
				final long endTime = System.nanoTime();

				System.out.println("Training " + a.getClass().getSimpleName() + " took "
						+ (endTime - startTime) / 1000000000.0 + "s.");
			}

			// step 4:
			// generate validation results
			final ArrayList<Results> roundResults = new ArrayList<Results>();
			final Map<Label, Integer> tSetDist = Services.countLabels(trainingFeatures);
			final Map<Label, Integer> vSetDist = Services.countLabels(validationFeatures);

			for (final Agent a : agents) {
				final HashMap<Label, Integer> pDist = new HashMap<Label, Integer>();

				// predict label for every feature in validation set
				for (final Entry<Feature, Label> e : validationFeatures.entrySet()) {
					Integer frequency = pDist.get(e.getValue());

					if (frequency == null) {
						frequency = 0;
					}

					final Label prediction = a.predict(e.getKey());

					if (prediction == null) {
						throw new Exception("No label predicted");
					}

					if (prediction.equals(e.getValue())) {
						++frequency;
					}

					pDist.put(e.getValue(), frequency);
				}

				roundResults.add(new Results(tSetDist, vSetDist, pDist, a));
			}

			results.put(i, roundResults);
		}

		return results;
	}
}